package com.lyx.server.service.impl;


import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWTUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.lyx.common.config.RedisKeyConfig;
import com.lyx.common.param.*;
import com.lyx.common.type.LevelType;
import com.lyx.common.vo.R;
import com.lyx.server.dao.*;
import com.lyx.server.entity.*;
import com.lyx.server.feign.CouponService;
import com.lyx.server.service.UserService;
import com.lyx.server.sms.AliSmsUtil;
import com.lyx.server.util.CodeUtil;
import com.lyx.server.util.DateUtils;
import com.lyx.server.util.ExpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author McMeloxD
 * @description 针对表【user(⽤户表)】的数据库操作Service实现
 * @createDate 2024-08-12 17:43:27
 */
@Slf4j
@Service("userService")
@RequiredArgsConstructor // 用于自动为类中所有被声明为 final 的成员变量生成一个构造器，并在构造器中初始化这些成员变量。
public class UserServiceImpl implements UserService {

    private final UserDao dao;
    private final UserLogDao userLogDao;
    private final UserInviteDao inviteDao;
    private final SmsLogDao smsLogDao;
    private final StringRedisTemplate template;
    private final UserKickbackDao kickbackDao;
    private final CouponService couponService;
    private final SignLogDao signLogDao;
    private final ExpUtil expUtil;

    @Override
    public R checkPhone(String phone) {
        // 1.校验 非空 手机号格式-正则
        if (StringUtils.hasLength(phone)) {
            // hasLength方法用于检查字符串是否不为null且长度大于0

            // 2.查询 验证手机号是否存在 不存在-可用
            if (dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)) == null) {
                // 可用
                return R.ok("");
            }
        }
        return R.fail("亲，请检查手机号！");
    }

    @Override
    public R sendRcode(String phone) {
        // 1.校验
        if (StringUtils.hasLength(phone)) {
            // 2.手机号 不存在
            // 这里用lambda表达式的写法,User::getPhone是为了调用该方法获取字段名
            if (dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)) == null) {
                // 3.生成验证码
                String code = RandomUtil.randomNumbers(6);
                log.error("验证码={}", code);// 记录日志
                // 4.发送短信
                // if (AliSmsUtil.sendCode(AliSmsUtil.TEM_RCODE, phone, code)) {
                if (true) { // 模拟假数据
                    // 5 存储验证码 使用Redis
                    // 五问：1.为什么使用 2.使用哪种数据类型 3.要不要设置有效期 4.存储什么样的内容 5.要不要考虑数据同步
                    // 1.快 有效期 2.String 单个 3.要 10分钟 4.key cp:sms:code:手机号 value 验证码 5.不需要
                    template.opsForValue().set(RedisKeyConfig.KEY_RCODE + phone, code, RedisKeyConfig.CODE_TIME, TimeUnit.MINUTES);

                    // 6.记录短信发送
                    smsLogDao.insert(new SmsLog(phone, "注册验证码", "实现注册验证码的发送"));

                    // 7.返回
                    return R.ok("请，查看手机");
                }
            }
        }
        return R.fail("亲，请检查手机号！");
    }

    @Override
    public R checkRcode(PhoneCode phoneCode) {
        // 1.验证是否存在
        String key = RedisKeyConfig.KEY_RCODE + phoneCode.getPhone();
        if (template.hasKey(key)) {
            // 2.比较验证码
            if (Objects.equals(phoneCode.getCode(), template.opsForValue().get(key))) {
                // 3.验证码比对成功 删除验证码
                template.delete(key);

                // 4.记录验证通过 的手机号
                template.opsForSet().add(RedisKeyConfig.KEY_RPHONES, phoneCode.getPhone());

                return R.ok("");
            }
        }
        return R.fail("亲，验证码无效！");
    }

    @Transactional // 开启事务 保证内部多个SQL 要么都成功，要么都失败
    @Override
    public R register(UserAdd userAdd) {
        // 1.参数校验 还需要校验参数的值是否符合
        if (userAdd == null) {
            return R.fail("参数错误");
        }
        // 2.校验注册手机号 有没有校验过验证码 isMember(key,value)方法用于判断一个特定的元素是否存在于 Redis 集合中。‌
        if (!template.opsForSet().isMember(RedisKeyConfig.KEY_RPHONES, userAdd.getPhone())) {
            return R.fail("亲,请完成验证码校验~ ");
        }
        // 3.生成 用户对象 本类里,nuser是新用户,下面ouser是老用户
        User nuser = new User();
        // 用springb自带的工具,拷贝一下对象
        BeanUtils.copyProperties(userAdd, nuser);
        // 密码 密文  实际上 最后使用 aes或rsa  SecureUtil
        nuser.setPassword(SecureUtil.sha1(SecureUtil.md5(nuser.getPassword())));
        nuser.setLevel(LevelType.低级代理商.getVal());
        nuser.setStatus("有效");
        nuser.setExp(0);
        nuser.setKickback(0.0);
        // 生成邀请码
        nuser.setCode(CodeUtil.createCode());
        nuser.setCreateTime(new Date());
        nuser.setUpdateTime(new Date());

        if (dao.insert(nuser) <= 0) {
            // 如果失败
            return R.fail("亲新增用户失败!");
        }

        // 4.新人有礼 优惠券 9折、充100送20、充值红包10元（满50）省略
        //.新用户：奖励10元充值红包（50起步）、奖励优惠券：1张9折卡1张充100送20
        couponService.addNew(nuser.getId());
        // 注册奖励积分50
        expUtil.addExp(nuser.getId(), 50, "注册奖励", "注册");
        // 5. 邀请码
        if (StringUtils.hasLength(userAdd.getInvcode())) {
            // 存在邀请码，就需要 给予邀请奖励
            // 2.拉取新用户：拉一个新用户，奖励：佣金10元，后续用户充值，可以获取5%的佣金
            // 3.被拉新的用户：奖励佣金5元
            User ouser = dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getCode, userAdd.getInvcode()));
            if (ouser != null) {
                // 拉新 奖励10元
                dao.updateKickback(10, ouser.getId());
                // 在佣金变动表 插入流水记录
                kickbackDao.insert(new UserKickback(ouser.getId(), "拉新奖励", 10.0, "邀请新人用户奖励10元"));
                // 拉新 奖励优惠券 满送劵 1张 满100送10，1张满500送100
                couponService.addAsk(ouser.getId());
                // 拉新奖励30积分
                expUtil.addExp(ouser.getId(), 30, "邀请新用户奖励", "拉新");
                // 被拉的人,奖励5元
                dao.updateKickback(5, nuser.getId());
                kickbackDao.insert(new UserKickback(nuser.getId(), "被拉新奖励", 5.0, "被邀请用户奖励5元"));
                // 记录邀请信息
                inviteDao.insert(new UserInvite(userAdd.getInvcode(), ouser.getId(), nuser.getId(), 10.0));
            }
        }
        // 6 生成日志
        userLogDao.insert(new UserLog(nuser.getId(), "注册", "注册新用户", "注册成功"));

        // 7 删除验证码校验成功的手机号
        template.opsForSet().remove(RedisKeyConfig.KEY_RPHONES, nuser.getPhone());
        return R.ok("成功");
    }

    @Transactional
    @Override
    public R login(UserLogin login) {
        // 1.校验,有校验无需判断
        // if (login == null) {
        //     return R.fail("参数错误");
        // }

        // 2.直接查询是否有该用户
        User user = dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, login.getPhone()));
        if (user == null) {
            return R.fail("亲，手机号或密码不正确！");
        }


        // 3 先判断是否已锁定
        if (template.hasKey(RedisKeyConfig.Lock_ERRORPHONE + user.getPhone())) {
            return R.fail("亲,您已被锁定，请2小时后再试~");
        }


        // 4.校验密码,密码错误,10分钟内3次失败就锁定,错误的时候先判断是否为第3次,是的话直接锁定
        if (!user.getPassword().equals(SecureUtil.sha1(SecureUtil.md5(login.getPassword())))) {
            // 先查是否是否是第一次错误
            if (template.hasKey(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone())) {
                // 如果有,就判断是否是第3次错误
                if (template.opsForValue().get(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone()).equals("3")) {
                    // 锁定,存入redis
                    template.opsForValue().set(RedisKeyConfig.Lock_ERRORPHONE + user.getPhone(), user.getName(), RedisKeyConfig.Lock_ERROR_TIME, TimeUnit.MINUTES);
                    // 清空错误次数
                    template.delete(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone());
                    // 存入userlog
                    userLogDao.insert(new UserLog(user.getId(), "登录", "账号锁定", "密码错误3次锁2小时"));
                    return R.fail("亲,您已被锁定，请2小时后再试~");
                } else {
                    // 如果不是第三次错误,就+1
                    template.opsForValue().increment(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone());
                    // 错误通知客户还有几次机会
                    int n = Integer.parseInt(template.opsForValue().get(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone()));
                    return R.fail("亲，手机号或密码不正确!您还有" + (3 - n) + "次机会,10分钟内3次密码错误将锁定您的账号!");
                }
            } else {
                // 第一次错误存入redis
                template.opsForValue().set(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone(), "1", RedisKeyConfig.PWD_ERROR_TIME, TimeUnit.MINUTES);
                return R.fail("亲，手机号或密码不正确!您还有2次机会,10分钟内3次密码错误将锁定您的账号!");
            }
        }

        // 5.校验当前账号是否已登录
        if (template.hasKey(RedisKeyConfig.KEY_RPHONES + user.getPhone())) {
            // 当前手机号 已在线
            // 两种解决方案：
            // 1.已在线，就不允许重复登录，除非退出
            // 2.已在线，可以登录，踢掉在线的客户端（手机app需要使用第三方推送工具，挤掉的推送，比如 极光等）
            return R.fail("亲,您已在其他地方登录~");
        }

        // 登录成功先清空密码错误次数
        template.delete(RedisKeyConfig.PWD_ERRORPHONE + user.getPhone());

        // 判断是否今日首次登录
        if (userLogDao.queryLastByType("登录", user.getId()) == null || !DateUtils.isSameDay(new Date(), userLogDao.queryLastByType("登录", user.getId()).getCreateTime()) ) {
            // 奖励积分
            expUtil.addExp(user.getId(), 10, "每次首次登录奖励", "登录");
        }

        // 6.生成令牌，微服务 前后端分离 令牌：Token，令牌生成算法：JWT算法
        Map map = new HashMap();
        map.put("id", user.getId());
        map.put("phone", user.getPhone());
        map.put("ctimes", System.currentTimeMillis());
        String token = JWTUtil.createToken(map, "cloudproject".getBytes());

        // 7.存储令牌
        user.setPassword(""); // 先把密码设为空,不返回密码
        // 记录令牌 对应的用户 作用：未来，其他地方需要通过令牌获取用户信息
        template.opsForValue().set(RedisKeyConfig.AUTH_TOKEN + token, JSONUtil.toJsonStr(user), RedisKeyConfig.TOKEN_TIME, TimeUnit.MINUTES);
        // 记录登录的手机号，作用 实现唯一登录校验
        template.opsForValue().set(RedisKeyConfig.KEY_RPHONES + user.getPhone(), token, RedisKeyConfig.TOKEN_TIME, TimeUnit.MINUTES);

        // 8.记录日志
        userLogDao.insert(new UserLog(user.getId(), "登录", "登录成功", "手机号密码登录成功"));
        // 9.返回信息
        Map<String, Object> result = new HashMap<>();
        // 令牌
        result.put("token", token);
        // 用户信息
        result.put("user", user);
        // 邀请用户的名称
        result.put("inviteName", inviteDao.selectByAskuid(user.getId()));
        // 返回
        return R.ok(result);
    }

    @Override
    public R logout(String token) {
        // 1验证是否存在令牌
        String key = RedisKeyConfig.AUTH_TOKEN + token;
        if (template.hasKey(key)) {
            // 如果存在,获取用户的信息
            User user = JSONUtil.toBean(template.opsForValue().get(key).toString(), User.class);
            // 删除
            template.delete(RedisKeyConfig.AUTH_PHONE + user.getPhone());
            template.delete(key);

            return R.ok("");
        }
        return R.fail("亲，请确认是否已登录！");
    }

    @Override
    public R queryAll() {
        return R.ok(dao.selectList(null));
    }

    @Override
    public R findPswSendRcode(String phone) {
        // 1.校验
        if (StringUtils.hasLength(phone)) {
            System.out.println(phone);
            // 2.手机号已存在
            if (dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)) != null) {
                // 3.生成验证码
                String code = RandomUtil.randomNumbers(6);
                // 4.发送短信
                if (AliSmsUtil.sendCode(AliSmsUtil.TEM_RCODE, phone, code)) {
                    // 5 存储验证码 使用Redis
                    template.opsForValue().set(RedisKeyConfig.KEY_RCODE + phone, code, RedisKeyConfig.CODE_TIME, TimeUnit.MINUTES);

                    // 6.记录短信发送
                    smsLogDao.insert(new SmsLog(phone, "找回密码", "找回密码的验证码的发送"));

                    // 7.返回
                    return R.ok("请查看手机");
                }
            }
            return R.fail("亲，该手机号未注册！");
        }
        return R.fail("亲，请检查手机号！");
    }


    @Transactional
    @Override
    public R findPassword(UserFindPwd user) {
        // 只要手机号验证码通过就直接修改密码,前端判断两次输入的新密码即可,无需验证旧密码
        // 1.参数校验 还需要校验参数的值是否符合
        if (user == null) {
            return R.fail("参数错误");
        }
        // 2.校验找回密码的手机号 有没有校验过验证码 isMember(key,value)方法用于判断一个特定的元素是否存在于 Redis 集合中。‌
        if (!template.opsForSet().isMember(RedisKeyConfig.KEY_RPHONES, user.getPhone())) {
            return R.fail("亲,请完成验证码校验~ ");
        }
        // 3 修改密码
        int i = dao.updatePassword((SecureUtil.sha1(SecureUtil.md5(user.getPassword()))), user.getPhone());
        if (i >= 0) {
            // 4 删除验证码校验成功的手机号
            template.opsForSet().remove(RedisKeyConfig.KEY_RPHONES, user.getPhone());
            // 5.查找userid
            User user1 = dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, user.getPhone()));
            // 6.记录日志
            userLogDao.insert(new UserLog(user1.getId(), "找回密码", "找回成功", "手机号密码找回成功"));
            return R.ok("修改成功");
        } else return R.fail("修改失败");
    }

    @Override
    public R bindEmail(String email) {

        return null;
    }

    @Transactional
    @Override
    public R updateInfo(UserUpdateInfo up) {
        if (up != null) {
            // 1.获取当前用户
            User user = dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, up.getPhone()));
            if (user != null) {
                // 2.修改用户信息
                user.setName(up.getName());
                // 3.修改数据库
                dao.updateNameByPhone(user.getName(), user.getPhone());
                // 4.记录日志
                userLogDao.insert(new UserLog(user.getId(), "修改信息", "修改成功", "修改用户信息"));
                // 5.返回
                return R.ok("修改成功");
            } else {
                return R.fail("请检查参数!");
            }

        }
        return R.fail("请检查参数!");
    }

    @Transactional
    @Override
    public R updatePassword(UserUpdatePsw uup) {
        // 前端校验两次密码不同才能发送请求,这里不做判断
        if (uup != null) {
            // 1.根据旧密码和手机号获取用户
            User user = dao.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, uup.getPhone()).eq(User::getPassword, SecureUtil.sha1(SecureUtil.md5(uup.getOldPsw()))));
            if (user != null) {
                // 2.修改用户密码
                user.setPassword(SecureUtil.sha1(SecureUtil.md5(uup.getNewPsw())));
                // 3.修改数据库
                dao.updatePassword(user.getPassword(), user.getPhone());
                // 4.记录日志
                userLogDao.insert(new UserLog(user.getId(), "修改密码", "修改成功", "修改用户密码"));
                return R.ok("修改成功");
            } else {
                return R.fail("当前密码错误!");
            }
        }
        return R.fail("参数错误!");
    }

    @Transactional
    @Override
    public R signIn(Integer uid) {
        if (uid != null) {
            // 1.获取当前用户
            User user = dao.selectById(uid);
            if (user != null) {
                //判断用户是否有过签到记录，没有就说明第一次签到
                if (signLogDao.queryByUid(uid, "签到") == null) {
                    // 2.签到
                    Random random = new Random();
                    int num = random.nextInt(3) + 3;
                    expUtil.addExp(uid, num, "签到奖励", "签到");
                    int n = dao.updateSdaysByUid(uid, 1);
                    if (n >= 0) {
                        return R.ok("签到成功,获得" + num + "积分,当前已连续签到1天!");
                    } else return R.fail("签到失败!");
                }
                // 不是第一次就判断用户今天是否已签到,从签到记录表里查该用户最新一条签到记录的日期是否等于今天
                if (!DateUtils.isSameDay(signLogDao.queryByUid(uid, "签到").getSTime(), new Date())) {
                    Random random = new Random();
                    // 判断用户是否在连续签到,日历对象获取昨天日期,然后比较
                    Calendar calendar = Calendar.getInstance();
                    calendar.add(Calendar.DAY_OF_MONTH, -1);
                    // 如果是昨天签到那说明在连续签到
                    if (DateUtils.isSameDay(signLogDao.queryByUid(uid, "签到").getSTime(), calendar.getTime())) {
                        // 判断当前连续签到几天,基础 3-5积分，连续3天6-10，连续7天12-20,连续1个月以上24-40
                        if (user.getSDays() >= 30) {
                            int num = random.nextInt(17) + 24;
                            expUtil.addExp(uid, num, "连续签到奖励", "签到");
                            int n = dao.updateSdaysByUid(uid, user.getSDays() + 1);
                            if (n >= 0) {
                                return R.ok("签到成功,获得" + num + "积分,当前已连续签到" + (user.getSDays() + 1) + "天!");
                            } else return R.fail("签到失败!");
                        } else if (user.getSDays() >= 7) {
                            int num = random.nextInt(9) + 12;
                            expUtil.addExp(uid, num, "连续签到奖励", "签到");
                            int n = dao.updateSdaysByUid(uid, user.getSDays() + 1);
                            if (n >= 0) {
                                return R.ok("签到成功,获得" + num + "积分,当前已连续签到" + (user.getSDays() + 1) + "天!");
                            } else return R.fail("签到失败!");
                        } else if (user.getSDays() >= 3) {
                            int num = random.nextInt(5) + 6;
                            expUtil.addExp(uid, num, "连续签到奖励", "签到");
                            int n = dao.updateSdaysByUid(uid, user.getSDays() + 1);
                            if (n >= 0) {
                                return R.ok("签到成功,获得" + num + "积分,当前已连续签到" + (user.getSDays() + 1) + "天!");
                            } else return R.fail("签到失败!");
                        } else {
                            int num = random.nextInt(3) + 3;
                            expUtil.addExp(uid, num, "签到奖励", "签到");
                            int n = dao.updateSdaysByUid(uid, user.getSDays() + 1);
                            if (n >= 0) {
                                return R.ok("签到成功,获得" + num + "积分,当前已连续签到" + (user.getSDays() + 1) + "天!");
                            } else return R.fail("签到失败!");
                        }
                    } else {
                        // 如果不是就说明断签了,从1开始,随机3-5积分
                        int num = random.nextInt(3) + 3;
                        expUtil.addExp(uid, num, "连续签到奖励", "签到");
                        int n = dao.updateSdaysByUid(uid, 1);
                        if (n >= 0) {
                            return R.ok("签到成功,获得" + num + "积分,当前已连续签到1天!");
                        } else return R.fail("签到失败!");
                    }
                } else return R.fail("亲,今天已签到!");
            } else return R.fail("获取用户信息失败!");
        }
        return R.fail("参数错误!");
    }


}




